﻿/*
 * OSCAR Auth Servers  -----------
 * login.oscar.aol.com      (default)
 * login.icq.com            (ICQ default)
 * ibucp-vip-m.blue.aol.com (?)

 * FLAP_C# works with TcpClient.Connect(), so you are not able to use Proxy to connect to ICQ.
 * http://www.mentalis.org/soft/class.qpx?id=9
 * Maybe in future i will add Proxy Support or you can try yourself..
 */

/*
 * CanonOscar 0.1
 * Written by (c) agehack a.k.a. GUnitSoldier
 * Released under GNU GPL (http://www.gnu.org/copyleft/gpl.html)
 * 
 * http://www.agehack.pro/oscar
 */
using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
using Grunwald.NOscar;
namespace Agehack.Icq
{
    /// <summary>
    /// OSCAR (ICQ/AIM) prototype written by (c) agehack.
    /// Released under GNU GPL.
    /// Febrary 2012, CanonOscar version 0.1
    /// http://www.agehack.pro/oscar
    /// </summary>
    public static partial class CanonOscar
    {
        /// <summary>
        /// FLAP id byte is always 0x2A. It is frame-start sign.
        /// FLAP 1.0 (and OSCAR) works only with this ID.
        /// </summary>
        public const byte C_OSCAR_FLAP_ID = 0x2a;
        /// <summary>
        /// Type of FLAP channels.
        /// </summary>
        public enum FlapChannel
        {
            /// <summary>
            /// On this channel client and server acting auth sequence (for default login method)   -NEED.INFO
            /// </summary>
            NewConnectionNegotiation    = 1,
            /// <summary>
            /// SNAC packets are sent only on this channel.
            /// SNACs are most used session commands.
            /// </summary>
            SNAC                        = 2,
            /// <summary>
            /// In this channel server sends FLAP-Level errors.
            /// (definetely something wrong with connection/transport version or etc.)
            /// We have not so much info about such packets. -NEED.INFO
            /// </summary>
            FlapError                   = 3,
            /// <summary>
            /// Probably deprecated or used before client/server closing connection. -NEED.INFO
            /// </summary>
            CloseConnectionNegotiation  = 4,
            /// <summary>
            /// Any of received KeepAlives are ignored by server. Server never sends keepAlives.
            /// </summary>
            KeepAlive                   = 5
        }

        /// <summary>
        /// A SNAC is the basic communication unit that is exchanged between clients and servers.
        /// The SNAC communication layers sits on top of the FLAP layer.
        /// SNAC is the normal contents of the FLAP Data Field for channel 0x02.
        /// There can be only one SNAC per FLAP frame.
        /// </summary>
        public class Snac
        {
            /// <summary>
            /// Size in bytes of Snac packet header.
            /// Snac header doesn't contain info about packet own body size.
            /// </summary>
            public const byte C_HEADER_SIZE = 10;

            #region Fields
            private ushort sFamId = 0;
            private ushort sFamSub = 0;
            private ushort sFlags = 0;
            private uint sRequestId = 0;
            private byte[] sBody = null;
            #endregion
            #region Constructors
            /// <summary>
            /// Creates new SNAC with properties.
            /// </summary>
            /// <param name="famId">SNAC FamilyID (Foodgroup)</param>
            /// <param name="famSub">SNAC FamilySubtype (service)</param>
            /// <param name="flags">Flags</param>
            /// <param name="requestId">RequestID</param>
            /// <param name="body">Body of this packet.</param>
            public Snac(ushort famId, ushort famSub, ushort flags, uint requestId, byte[] body)
            {
                this.sFamId = famId;
                this.sFamSub = famSub;
                this.sFlags = flags;
                this.sRequestId = requestId;
                this.sBody = body;
            }
            /// <summary>
            /// Creates new SNAC with properties.
            /// </summary>
            /// <param name="famId">SNAC FamilyID (Foodgroup)</param>
            /// <param name="famSub">SNAC FamilySubtype (service)</param>
            /// <param name="requestId">RequestID</param>
            /// <param name="body">Body of this packet.</param>
            public Snac(ushort famId, ushort famSub, uint requestId, byte[] body)
                : this(famId, famSub, 0, requestId, body) { }
            /// <summary>
            /// Creates new SNAC with properties.
            /// </summary>
            /// <param name="famId">SNAC FamilyID (Foodgroup)</param>
            /// <param name="famSub">SNAC FamilySubtype (service)</param>
            /// <param name="body">Body of this packet.</param>
            public Snac(ushort famId, ushort famSub, byte[] body)
                : this(famId, famSub, 0, 0, body) { }
            /// <summary>
            /// Creates new SNAC with properties.
            /// </summary>
            /// <param name="famId">SNAC FamilyID (Foodgroup)</param>
            /// <param name="famSub">SNAC FamilySubtype (service)</param>
            /// <param name="flags">Flags</param>
            public Snac(ushort famId, ushort famSub, ushort flags)
                : this(famId, famSub, flags, 0, null) { }
            /// <summary>
            /// Creates new SNAC with properties.
            /// </summary>
            /// <param name="famId">SNAC FamilyID (Foodgroup)</param>
            /// <param name="famSub">SNAC FamilySubtype (service)</param>
            public Snac(ushort famId, ushort famSub)
                : this(famId, famSub, 0, 0, null) { }
            #endregion
            #region Properties
            /// <summary>
            /// Family (service) id number.
            /// By official documentation it is called 'Foodgroup'.
            /// </summary>
            public ushort Family        { get { return sFamId; } set { sFamId = value; } }
            /// <summary>
            /// Family subtype id number.
            /// </summary>
            public ushort Subtype       { get { return sFamSub; } set { sFamSub = value; } }
            /// <summary>
            /// Command flags... -NEED.INFO
            /// </summary>
            public ushort Flags         { get { return sFlags; } set { sFlags = value; } }
            /// <summary>
            /// The client can generate completely random reqid's as long as it remembers what the request was for.
            /// Often, though, the results of the SNAC are irrelevant, and the reqid's can be forgotten.
            /// But, in information-requestion SNACs, it is imperative you remember the reqid you sent because that's the only way to link it to the response!
            /// <br /><br />
            /// Seems like server doesn't use RequestIDs in it's requests to a client. -NEED.INFO
            /// </summary>
            public uint RequestId       { get { return sRequestId; } set { sRequestId = value; } }
            /// <summary>
            /// Body of SNAC packet containing it's info.
            /// Maximum size limit isn't not checked, but we can think it is 10 bytes less than FLAP's one.
            /// </summary>
            public byte[] Body          { get { return sBody; } set { sBody = value; } }
            /// <summary>
            /// Current size of packet Body.
            /// </summary>
            public int BodySize         { get { return Body.Length; } }
            #endregion
            #region Methods
            /// <summary>
            /// Builds a [ header+body ] array represents this SNAC packet.
            /// It can be converted back to Snac format by static Parse() method.
            /// It can be sent in FLAP packet body.
            /// </summary>
            /// <returns>Byte array ready to be sent. It contains all SNAC data.</returns>
            public byte[] GetBytes()
            {
                byte[] result = new byte[BodySize + C_HEADER_SIZE];
                util.merge(result,
                    BitConverter.GetBytes(sFamId),
                    0, out result);
                util.merge(result,
                    BitConverter.GetBytes(sFamSub),
                    2, out result);
                util.merge(result,
                    BitConverter.GetBytes(sFlags),
                    4, out result);
                util.merge(result,
                    BitConverter.GetBytes(sRequestId),
                    6, out result);
                util.merge(result, sBody,
                    C_HEADER_SIZE, out result);
                return result;
            }
            /// <summary>
            /// Increases BodySize and writes [add] array to the end of [Body].
            /// </summary>
            /// <param name="add">Data to add to the end of Body.</param>
            public void AppendBody(byte[] add)
            {
                if (Body == null)
                    Body = add;
                else
                {
                    byte[] newbody = new byte[BodySize + add.Length];
                    util.merge(newbody, Body, 0);
                    util.merge(newbody, add, BodySize, out newbody);
                    Body = newbody;
                }
            }
            #endregion

            /// <summary>
            /// Reads and parses SNAC packet from bytes array.
            /// It can be FLAP packet body.
            /// </summary>
            /// <param name="recv">Bytes array to parse from</param>
            /// <returns></returns>
            public static Snac Parse(byte[] recv)
            {
                Snac s = new Snac(
                    BitConverter.ToUInt16(recv, 0),
                    BitConverter.ToUInt16(recv, 2),
                    null);
                s.Flags = BitConverter.ToUInt16(recv, 4);
                s.RequestId = BitConverter.ToUInt32(recv, 6);
                s.Body = util.subarray(recv, C_HEADER_SIZE);
                return s;
            }
        }
        /// <summary>
        ///  This class is [sealed] because it isn't safe to create affiliated types.
        ///  <br/>
        ///  TLVs are a very convenient and efficient method of putting data into an organized format, especially variable length strings, etc.
        ///  TLV literally stands for "Type, Length, Value".
        ///  <br/>
        ///  In OSCAR protocol much of SNACs contain TLVs.
        /// </summary>
        public sealed class Tlv
        {
            /// <summary>
            /// Size of each Tlv packet header.
            /// </summary>
            public const byte C_HEADER_SIZE = 4;

            #region Fields & Properties
            private ushort tTypeNum = 0;
            private byte[] tData = null;
            /// <summary>
            /// TLV type identification number.
            /// </summary>
            public ushort TypeNumber { get { return tTypeNum; } set { tTypeNum = value; } }
            /// <summary>
            /// Formally known as 'Value-Length', means size of data inside this Tlv.
            /// </summary>
            public ushort DataSize { get { return (ushort)tData.Length; } }
            /// <summary>
            /// Formally known as 'Value'.
            /// </summary>
            public byte[] Data { get { return tData; } set { tData = value; } }
            #endregion
            #region Constructors
            /// <summary>
            /// Creates new TLV packet
            /// </summary>
            /// <param name="typeNumber">TLV TypeNumber</param>
            /// <param name="data">Data for this Tlv</param>
            public Tlv(ushort typeNumber, byte[] data)
            {
                tTypeNum = typeNumber;
                tData = data;
            }
            /// <summary>
            /// Creates new TLV packet (Data field will be set to null)
            /// </summary>
            /// <param name="typeNumber">TLV TypeNumber</param>
            public Tlv(ushort typeNumber)
                : this(typeNumber, null) { }
            /// <summary>
            /// Creates new TLV packet with text data
            /// </summary>
            /// <param name="typeNumber">TLV TypeNumber</param>
            /// <param name="text">Text to encode</param>
            /// <param name="encd">Encoding to usefor this text</param>
            public Tlv(ushort typeNumber, string text, Encoding encd)
                : this(typeNumber, encd.GetBytes(text)) { }
            /// <summary>
            /// Creates new TLV packet with text data
            /// </summary>
            /// <param name="typeNumber">TLV TypeNumber</param>
            /// <param name="number">word(ushort) value to save in data field.</param>
            public Tlv(ushort typeNumber, ushort number)
                : this(typeNumber, BitConverter.GetBytes(number)) { }
            #endregion
            #region Methods
            /// <summary>
            /// Returns array of bytes representing this packet.
            /// </summary>
            /// <returns></returns>
            public byte[] GetBytes()
            {
                byte[] arr = new byte[C_HEADER_SIZE + DataSize];

                util.merge(arr,
                    BitConverter.GetBytes(TypeNumber), 0, out arr);
                util.merge(arr,
                    BitConverter.GetBytes(DataSize), 2, out arr);

                util.merge(arr, Data, C_HEADER_SIZE, out arr);
                return arr;
            }
            /// <summary>
            /// Returns data of this TLV as text saved in specified encoding.
            /// </summary>
            /// <param name="encd">Encoding of text.</param>
            /// <returns>String with text</returns>
            public string GetString(Encoding encd)
            {
                return encd.GetString(Data);
            }
            /// <summary>
            /// Sets to data of this Tlv text in specified encoding.
            /// </summary>
            /// <param name="encd">Encoding of text.</param>
            /// <param name="str">String to set.</param>
            public void SetString(Encoding encd, string str)
            {
                Data = encd.GetBytes(str);
            }
            #endregion

            /// <summary>
            /// Reads and parses Tlv packet from bytes array.
            /// </summary>
            /// <param name="array">Array of bytes to parse from (start_pos=0)</param>
            /// <param name="end_position">After completed, saves the end position where read of single packet finished.
            /// Next packet in this array should be read from this position.</param>
            /// <returns>Built TLV packet.</returns>
            public static Tlv Parse(byte[] array, out int end_position)
            {
                Tlv t = new Tlv(
                    BitConverter.ToUInt16(array, 0)
                    );
                ushort datasize = BitConverter.ToUInt16(array, 2);
                t.Data = util.subarray(array, C_HEADER_SIZE, datasize);
                end_position = datasize + C_HEADER_SIZE;
                return t;
            }
            /// <summary>
            /// Reads and parses Tlv packet from bytes array.
            /// </summary>
            /// <param name="array">Array of bytes to parse from</param>
            /// <returns>Built TLV packet.</returns>
            public static Tlv Parse(byte[] array)
            {
                int i = 0;
                return Parse(array, out i);
            }
            public override string ToString()
            {
                return Encoding.ASCII.GetString(Data);
            }
        }
        /// <summary>
        /// Contains several TLVs.
        /// Such lists are going to be added to SNAC packet body.
        /// </summary>
        public sealed class TlvList
        {
            #region General
            private List<Tlv> tlvs = null;
            /// <summary>
            /// You can access any TLV in list by it's index.
            /// </summary>
            /// <param name="index">Index of TLV in list</param>
            /// <returns>Tlv at index in list</returns>
            public Tlv this[int index] { get { return tlvs[index]; } }
            /// <summary>
            /// You can access any TLV in list by it's typeNumber.
            /// </summary>
            /// <param name="typeNumber">typeNumber of TLV in list</param>
            /// <returns>Tlv of this type or null if not found</returns>
            public Tlv this[ushort typeNumber]
            {
                get
                {
                    int index = Find(typeNumber);
                    if (index > 0)
                        return this[index];
                    else return null;
                }
            }
            /// <summary>
            /// Creates empty TlvList.
            /// </summary>
            public TlvList() { tlvs = new List<Tlv>(); }
            /// <summary>
            /// Creates TlvList containing TLVs passed to contructor (null TLVs skipped)
            /// </summary>
            public TlvList(Tlv tlv1, Tlv tlv2, Tlv tlv3)
                : this()
            {
                Add(tlv1);
                Add(tlv2);
                Add(tlv3);
            }
            /// <summary>
            /// Creates TlvList containing TLVs passed to contructor (null TLVs skipped)
            /// </summary>
            public TlvList(Tlv tlv1, Tlv tlv2)
                : this(tlv1, tlv2, null) { }
            #endregion
            #region List management
            /// <summary>
            /// Adds new Tlv to the end of list.
            /// </summary>
            /// <param name="tlv">Tlv to add</param>
            public void Add(Tlv tlv) { if (tlv != null) tlvs.Add(tlv); }
            /// <summary>
            /// Creates and adds new Tlv to the end of list.
            /// </summary>
            /// <param name="typeNumber">TypeNumber of new Tlv</param>
            /// <param name="data">Contents of new Tlv</param>
            public void Add(ushort typeNumber, byte[] data) { Add(new Tlv(typeNumber, data)); }
            /// <summary>
            /// Removes Tlv from list by index.
            /// </summary>
            /// <param name="index"></param>
            public void RemoveAt(int index) { tlvs.RemoveAt(index); }
            /// <summary>
            /// Removes all TLVs from list.
            /// </summary>
            public void Clear() { tlvs.Clear(); }
            /// <summary>
            /// Returns count of TLVs inside this list.
            /// </summary>
            public int Count { get { return tlvs.Count; } }
            /// <summary>
            /// Returns list of TLVs
            /// </summary>
            /// <returns></returns>
            public List<Tlv> ToList() { return tlvs; }
            #endregion
            #region Data management
            // Work with TLVs as quickly as we can imagine.
            public static Encoding TextEncoding = Encoding.GetEncoding("ISO8859-1");
            public int Find(ushort typeNumber)
            {
                for (int i = 0; i < tlvs.Count; i++)
                    if (this[i].TypeNumber == typeNumber)
                        return i;
                return -1;
            }
            public void SetText(ushort typeNumber, string text)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                {
                    Add(typeNumber, null);
                    SetText(typeNumber, text);
                }
                else
                    this[tlvid].SetString(TextEncoding, text);
            }
            public void SetData(ushort typeNumber, byte[] data)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                {
                    Add(typeNumber, null);
                    SetData(typeNumber, data);
                }
                else
                    this[tlvid].Data = data;
            }
            public void SetValue(ushort typeNumber, byte b)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                {
                    Add(typeNumber, null);
                    SetValue(typeNumber, b);
                }
                else
                    this[tlvid].Data = new[] { b };
            }
            public void SetValue(ushort typeNumber, ushort number)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                {
                    Add(typeNumber, null);
                    SetValue(typeNumber, number);
                }
                else
                    this[tlvid].Data = NetworkBitConverter.GetBytes(number);
            }
            public void SetValue(ushort typeNumber, uint number)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                {
                    Add(typeNumber, null);
                    SetValue(typeNumber, number);
                }
                else
                    this[tlvid].Data = NetworkBitConverter.GetBytes(number);
            }
            public string GetText(ushort typeNumber)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                    return null;
                else
                    return this[tlvid].GetString(TextEncoding);
            }
            public ushort GetWORD(ushort typeNumber, ushort defaultValue)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                    return defaultValue;
                else
                    return NetworkBitConverter.ToUInt16(this[tlvid].Data, 0);
            }
            public uint GetDWORD(ushort typeNumber, uint defaultValue)
            {
                int tlvid = Find(typeNumber);
                if (tlvid < 0)
                    return defaultValue;
                else
                    return NetworkBitConverter.ToUInt32(this[tlvid].Data, 0);
            }
            #endregion
            #region Parsing and building
            /// <summary>
            /// Returns array of bytes contains all TLVs in this list in it's order.
            /// This array are normally should be put inside SNAC packet body.
            /// </summary>
            public byte[] GetBytes()
            {
                int bytes = 0;
                List<byte[]> buf = new List<byte[]>();
                foreach (Tlv t in tlvs)
                {
                    byte[] body = t.GetBytes();
                    bytes += body.Length;
                    buf.Add(body);
                }
                byte[] listbytes = new byte[bytes];
                int prev = 0;
                for (int i = 0; i < buf.Count; i++)
                {
                    util.merge(listbytes, buf[i], prev, out listbytes);
                    prev += buf[i].Length;
                }
                return listbytes;
            }
            /// <summary>
            /// Calculates size of all TLVs data
            /// </summary>
            /// <returns>Size of this TLVs</returns>
            public int GetDataBytesSize()
            {
                return GetBytes().Length;
            }
            /// <summary>
            /// Adds TLV(or several TLVs) to this list parsed from bytes array.
            /// </summary>
            /// <param name="bytes">Bytes array contains TLV(s)</param>
            /// <returns>Count of parsed/added TLVs</returns>
            public int AppendList(byte[] bytes)
            {
                int stop = 0;
                int added = 0;
                while (stop != bytes.Length)
                {
                    int stopi = 0;
                    Tlv t = Tlv.Parse(
                        util.subarray(bytes, stop), out stopi);
                    stop += stopi;
                    Add(t);
                    added++;
                }
                return added;
            }
            /// <summary>
            /// Creates list from bytes array, it can be SNAC packet body which contains TLVs.
            /// </summary>
            /// <param name="bytes">Array contains TLVs (for example SNAC packet body)</param>
            /// <returns>Parsed TlvList object.</returns>
            public static TlvList Parse(byte[] bytes)
            {
                TlvList list = new TlvList();
                list.AppendList(bytes);
                return list;
            }
            /// <summary>
            /// Creates SNAC packet with set properties.
            /// It will contain TLVs from this list as packet body.
            /// </summary>
            /// <param name="famid">SNAC FamilyId (Foodgroup)</param>
            /// <param name="subtype">SNAC Family Subtype</param>
            /// <param name="flags">SNAC Flags..</param>
            /// <param name="requestId">SNAC packet RequestId</param>
            /// <returns>Built Snac packet containing all TLVs from this list.</returns>
            public Snac GetSnac(ushort famid, ushort subtype, ushort flags, uint requestId)
            {
                return new Snac(
                    famid, subtype, flags, requestId,
                    GetBytes());
            }
            #endregion
        }
    }
}
